/*
 * Copyright (C) 2016-2019 Samuel Wall
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package uk.co.samuelwall.materialtaptargetprompt;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.fraction.Fraction;
import ohos.accessibility.AccessibilityEventInfo;
import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorGroup;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.DependentLayout;
import ohos.agp.render.Canvas;
import ohos.agp.render.Path;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.multimodalinput.event.KeyEvent;
import ohos.multimodalinput.event.TouchEvent;
import uk.co.samuelwall.materialtaptargetprompt.extras.PromptOptions;
import uk.co.samuelwall.materialtaptargetprompt.extras.StyleBean;
import uk.co.samuelwall.materialtaptargetprompt.utils.Constants;

import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

/**
 * A Material Design tap target onboarding implementation.
 * <div class="special reference">
 * <p>For more information about onboarding and tap targets, read the
 * Material Design guidelines.</p>
 * </div>
 *
 * @since 2021-06-01
 */
public class MaterialTapTargetPrompt {
    /**
     * Prompt has yet to be shown.
     */
    public static final int STATE_NOT_SHOWN = 0;

    /**
     * Prompt is reveal animation is running.
     */
    public static final int STATE_REVEALING = 1;

    /**
     * Prompt reveal animation has finished and the prompt is displayed.
     */
    public static final int STATE_REVEALED = 2;

    /**
     * The prompt target has been pressed in the focal area.
     */
    public static final int STATE_FOCAL_PRESSED = 3;

    /**
     * The prompt has been removed from view after the prompt has been pressed in the focal area.
     */
    public static final int STATE_FINISHED = 4;

    /**
     * The {@link #dismiss()} method has been called and the prompt is being removed from view.
     */
    public static final int STATE_DISMISSING = 5;

    /**
     * The prompt has been removed from view after the prompt has either been pressed somewhere
     * other than the prompt target or the system back button has been pressed.
     */
    public static final int STATE_DISMISSED = 6;

    /**
     * The {@link #finish()} method has been called and the prompt is being removed from view.
     */
    public static final int STATE_FINISHING = 7;

    /**
     * The prompt has been pressed outside the focal area.
     */
    public static final int STATE_NON_FOCAL_PRESSED = 8;

    /**
     * The prompt has been dismissed by the show for timeout.
     */
    public static final int STATE_SHOW_FOR_TIMEOUT = 9;

    /**
     * The prompt has been dismissed by the system back button being pressed.
     */
    public static final int STATE_BACK_BUTTON_PRESSED = 10;

    /**
     * The prompt target has been pressed in the focal area.
     */
    public static final int STATE_FOCAL_BACKGROUND_PRESSED = 11;

    /**
     * The view that renders the prompt.
     */
    PromptView mView;

    /**
     * Used to calculate the animation progress for the reveal and dismiss animations.
     */
    @Nullable
    AnimatorValue mAnimationCurrent;

    /**
     * Used to calculate the animation progress for the idle breathing focal animation.
     */
    @Nullable
    AnimatorGroup mAnimationFocalBreathing;

    /**
     * Used to calculate the animation progress for the idle white flash animation.
     */
    @Nullable
    AnimatorValue mAnimationFocalRipple;

    /**
     * The last percentage progress for idle animation.
     * Value between 1 to 0 inclusive.
     * Used in the idle animation to track when the animation should change direction.
     */
    float mFocalRippleProgress;

    /**
     * The prompt's current state.
     */
    int mState;

    /**
     * The system status bar height.
     */
    final float mStatusBarHeight;

    /**
     * Listener for the view layout changing.
     */
    @Nullable
    final Component.LayoutRefreshedListener mGlobalLayoutListener;

    /**
     * Task used for triggering the prompt timeout.
     */
    final Runnable mTimeoutRunnable = () -> {
        // Emit the state change and dismiss the prompt
        onPromptStateChanged(STATE_SHOW_FOR_TIMEOUT);
        dismiss();
    };

    /**
     * Default constructor.
     *
     * @param promptOptions The options used to create the prompt.
     */

    MaterialTapTargetPrompt(final PromptOptions promptOptions) {
        final ResourceFinder resourceFinder = promptOptions.getResourceFinder();
        mView = new PromptView(resourceFinder.getContext());
        mView.mPrompt = this;
        mView.mPromptOptions = promptOptions;
        mView.setComponentDescription(promptOptions.getContentDescription());
        mView.mPromptTouchedListener = new PromptView.PromptTouchedListener() {
            @Override
            public void onFocalPressed() {
                if (!isDismissing()) {
                    onPromptStateChanged(STATE_FOCAL_PRESSED);
                    if (mView.mPromptOptions.getAutoFinish()) {
                        finish();
                    }
                }
            }

            @Override
            public void onFocalBackgroundPressed() {
                if (!isDismissing()) {
                    onPromptStateChanged(STATE_FOCAL_BACKGROUND_PRESSED);
                    if (mView.mPromptOptions.getAutoFinish()) {
                        dismiss();
                    }
                }
            }

            @Override
            public void onNonFocalPressed() {
                if (!isDismissing()) {
                    onPromptStateChanged(STATE_NON_FOCAL_PRESSED);
                    if (mView.mPromptOptions.getAutoDismiss()) {
                        dismiss();
                    }
                }
            }

            @Override
            public void onBackButtonPressed() {
                if (!isDismissing()) {
                    onPromptStateChanged(STATE_BACK_BUTTON_PRESSED);
                    onPromptStateChanged(STATE_NON_FOCAL_PRESSED);
                    if (mView.mPromptOptions.getAutoDismiss()) {
                        dismiss();
                    }
                }
            }
        };

        Rect rect = new Rect();
        resourceFinder.getPromptParentView().getWindowVisibleRect(rect);
        mStatusBarHeight = mView.mPromptOptions.getIgnoreStatusBar() ? 0 : rect.top;

        mGlobalLayoutListener = component -> {
            final Component targetView = mView.mPromptOptions.getTargetView();
            if (targetView != null) {
                if (!targetView.isBoundToWindow()) {
                    return;
                }
            }
            prepare();
            if (mAnimationCurrent == null) {
                // Force a relayout to update the view's location
                updateAnimation(1, 1);
            }
        };
    }

    /**
     * Displays the prompt.
     */
    public void show() {
        if (isStarting()) {
            return;
        }
        final ComponentContainer parent = mView.mPromptOptions.getResourceFinder().getPromptParentView();

        // If dismissing or the prompt already exists in the parent view
        if (isDismissing() || parent.findComponentById(ResourceTable.Id_material_target_prompt_view) != null) {
            cleanUpPrompt(mState);
        }
        if (mView.mPromptOptions.getTargetView() != null && mView.mPromptOptions.getIconDrawable() == null) {
            parent.addComponent(mView);
        } else {
            DependentLayout dependentLayout = new DependentLayout(parent.getContext());
            dependentLayout.addComponent(mView);
            Component targetView = mView.mPromptOptions.getTargetView();
            Drawable iconDrawable = mView.mPromptOptions.getIconDrawable();
            if (targetView != null && iconDrawable != null) {
                iconDrawable.setClickedListener(
                        component -> targetView.callOnClick());
            }
            dependentLayout.addComponent(mView.mPromptOptions.getIconDrawable());
            parent.addComponent(dependentLayout);
        }
        addGlobalLayoutListener();
        onPromptStateChanged(STATE_REVEALING);
        prepare();
        startRevealAnimation();
    }

    /**
     * Displays the prompt for a maximum amount of time.
     *
     * @param millis The number of milliseconds to show the prompt for.
     */
    public void showFor(long millis) {
        mView.postDelayed(mTimeoutRunnable, millis);
        show();
    }

    /**
     * Cancel the show for timer if it has been created.
     */
    public void cancelShowForTimer() {
        mView.removeCallbacks(mTimeoutRunnable);
    }

    /**
     * Get the current state of the prompt.
     *
     * @return mState
     * @see #STATE_NOT_SHOWN
     * @see #STATE_REVEALING
     * @see #STATE_REVEALED
     * @see #STATE_FOCAL_PRESSED
     * @see #STATE_NON_FOCAL_PRESSED
     * @see #STATE_BACK_BUTTON_PRESSED
     * @see #STATE_FINISHING
     * @see #STATE_FINISHED
     * @see #STATE_DISMISSING
     * @see #STATE_DISMISSED
     */
    public int getState() {
        return mState;
    }

    /**
     * Is the current state {@link #STATE_REVEALING} or {@link #STATE_REVEALED}.
     *
     * @return True if revealing or revealed.
     */
    boolean isStarting() {
        return mState == STATE_REVEALING || mState == STATE_REVEALED;
    }

    /**
     * Is the current state {@link #STATE_DISMISSING} or {@link #STATE_FINISHING}.
     *
     * @return True if dismissing or finishing.
     */
    boolean isDismissing() {
        return mState == STATE_DISMISSING || mState == STATE_FINISHING;
    }

    /**
     * Is the current state {@link #STATE_DISMISSED} or {@link #STATE_FINISHED}.
     *
     * @return True if dismissed or finished.
     */
    boolean isDismissed() {
        return mState == STATE_DISMISSED || mState == STATE_FINISHED;
    }

    /**
     * Is the current state neither {@link #STATE_REVEALED} or {@link #STATE_REVEALING}.
     *
     * @return True if not revealed or revealing.
     */
    boolean isComplete() {
        return mState == STATE_NOT_SHOWN || isDismissing() || isDismissed();
    }

    /**
     * Adds layout listener to view parent to capture layout changes.
     */
    void addGlobalLayoutListener() {
        mView.setLayoutRefreshedListener(mGlobalLayoutListener);
    }

    /**
     * Removes global layout listener added in {@link #addGlobalLayoutListener()}.
     */
    void removeGlobalLayoutListener() {
        final ComponentContainer parent = (ComponentContainer) mView.getComponentParent();
        if (parent == null) {
            return;
        }
        mView.setLayoutRefreshedListener(null);
    }

    /**
     * Removes the prompt from view, using a expand and fade animation.
     * <p>
     * This is treated as if the user has touched the target focal point.
     */
    public void finish() {
        if (isComplete()) {
            return;
        }
        cancelShowForTimer();
        cleanUpAnimation();
        mAnimationCurrent = new AnimatorValue();
        mAnimationCurrent.setDuration(Constants.NUM_225);
        mAnimationCurrent.setCurveType(mView.mPromptOptions.getAnimationCurveType());
        mAnimationCurrent.setValueUpdateListener((animatorValue, value) -> updateAnimation(
                1f + (value / Constants.NUM_4), 1 - value));
        mAnimationCurrent.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {
            }

            @Override
            public void onEnd(Animator animator) {
                cleanUpPrompt(STATE_FINISHED);
                mView.notifyAccessibility(AccessibilityEventInfo.WINDOWS_CHANGE_LAYER);
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });
        onPromptStateChanged(STATE_FINISHING);
        mAnimationCurrent.start();
    }

    /**
     * Removes the prompt from view, using a contract and fade animation.
     * <p>
     * This is treated as if the user has touched outside the target focal point.
     */
    public void dismiss() {
        if (isComplete()) {
            return;
        }
        cancelShowForTimer();
        cleanUpAnimation();
        mAnimationCurrent = new AnimatorValue();
        mAnimationCurrent.setDuration(Constants.NUM_225);
        mAnimationCurrent.setCurveType(mView.mPromptOptions.getAnimationCurveType());
        mAnimationCurrent.setValueUpdateListener((animatorValue, value) ->
                updateAnimation(1 - value, 1 - value));
        mAnimationCurrent.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {
            }

            @Override
            public void onEnd(Animator animator) {
                cleanUpPrompt(STATE_DISMISSED);
                mView.notifyAccessibility(AccessibilityEventInfo.WINDOWS_CHANGE_LAYER);
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });
        onPromptStateChanged(STATE_DISMISSING);
        mAnimationCurrent.start();
    }

    /**
     * Removes the prompt from view and triggers the {@link #onPromptStateChanged(int)} event.
     *
     * @param state state
     */
    void cleanUpPrompt(final int state) {
        cleanUpAnimation();
        removeGlobalLayoutListener();
        final ComponentContainer parent = (ComponentContainer) mView.getComponentParent();
        if (parent != null) {
            parent.removeComponent(mView);
        }
        if (isDismissing()) {
            onPromptStateChanged(state);
        }
    }

    /**
     * Stops any current animation and removes references to it.
     */
    void cleanUpAnimation() {
        if (mAnimationCurrent != null) {
            mAnimationCurrent.cancel();
            mAnimationCurrent.release();
            mAnimationCurrent = null;
        }
        if (mAnimationFocalRipple != null) {
            mAnimationFocalRipple.cancel();
            mAnimationFocalRipple = null;
        }
        if (mAnimationFocalBreathing != null) {
            mAnimationFocalBreathing.cancel();
            mAnimationFocalBreathing = null;
        }
    }

    /**
     * Starts the animation to reveal the prompt.
     */
    void startRevealAnimation() {
        updateAnimation(0, 0);
        cleanUpAnimation();
        mAnimationCurrent = new AnimatorValue();
        mAnimationCurrent.setCurveType(mView.mPromptOptions.getAnimationCurveType());
        mAnimationCurrent.setDuration(Constants.NUM_225);
        mAnimationCurrent.setValueUpdateListener((animatorValue, value) -> updateAnimation(value, value));
        mAnimationCurrent.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {

            }

            @Override
            public void onEnd(Animator animator) {
                updateAnimation(1, 1);
                cleanUpAnimation();
                if (mView.mPromptOptions.getIdleAnimationEnabled()) {
                    startIdleAnimations();
                }
                onPromptStateChanged(STATE_REVEALED);

                mView.requestFocus();
                mView.notifyAccessibility(AccessibilityEventInfo.TYPE_VIEW_FOCUSED_EVENT);
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });
        mAnimationCurrent.start();
    }

    /**
     * Starts the prompt idle animations.
     */
    void startIdleAnimations() {
        cleanUpAnimation();
        mAnimationFocalBreathing = new AnimatorGroup();
        mAnimationFocalBreathing.setCurveType(mView.mPromptOptions.getAnimationCurveType());
        mAnimationFocalBreathing.setDuration(Constants.NUM_1000);
        mAnimationFocalBreathing.setLoopedCount(AnimatorValue.INFINITE);
        AnimatorValue animatorValue1 = new AnimatorValue();
        animatorValue1.setValueUpdateListener((animatorValue, value) ->
                onAnimationUpdate(1 + Constants.NUM_01F * value, false));
        AnimatorValue animatorValue2 = new AnimatorValue();
        animatorValue2.setValueUpdateListener((animatorValue, value) ->
                onAnimationUpdate(Constants.NUM_1_1F - Constants.NUM_01F * value, true));
        mAnimationFocalBreathing.runSerially(animatorValue1, animatorValue2);
        mAnimationFocalRipple = new AnimatorValue();
        mAnimationFocalRipple.setCurveType(mView.mPromptOptions.getAnimationCurveType());
        mAnimationFocalRipple.setDuration(Constants.NUM_500);
        mAnimationFocalRipple.setValueUpdateListener((animation, value) -> {
            mView.mPromptOptions.getPromptFocal().updateRipple(Constants.NUM_1_1F
                            + Constants.NUM_05F * value,
                    Constants.NUM_05F * (1 - value) * Constants.NUM_2);
        });
        mAnimationFocalBreathing.start();
    }

    private void onAnimationUpdate(float newFocalFraction, boolean isDirection) {
        if (mAnimationFocalRipple != null && !mAnimationFocalRipple.isRunning() && !isDirection) {
            mAnimationFocalRipple.start();
        }
        mFocalRippleProgress = newFocalFraction;
        mView.mPromptOptions.getPromptFocal().update(mFocalRippleProgress, 1);
        mView.invalidate();
    }

    /**
     * Updates the positioning and alpha values using the animation values.
     *
     * @param revealModifier The amount to modify the reveal size, between 0 and 1.
     * @param alphaModifier  The amount to modify the alpha value, between 0 and 1.
     */
    void updateAnimation(final float revealModifier, final float alphaModifier) {
        if (mView.getComponentParent() == null) {
            return;
        }
        mView.mPromptOptions.getPromptText().update(revealModifier, alphaModifier);
        Component targetView = mView.mPromptOptions.getTargetView();
        if (targetView != null) {
            if (mView.mIconDrawable != null) {
                mView.mIconDrawable.setAlpha(alphaModifier);
                mView.mIconDrawable.setWidth(
                        (int) (targetView.getWidth() * alphaModifier));
                mView.mIconDrawable.setHeight(
                        (int) (targetView.getHeight() * alphaModifier));
            }
            mView.mPromptOptions.getPromptFocal().update(revealModifier, alphaModifier);
            mView.mPromptOptions.getPromptBackground().update(revealModifier, alphaModifier);
            mView.invalidate();
        }
    }

    /**
     * Update the focal and text positioning.
     */
    void prepare() {
        final Component targetRenderView = mView.mPromptOptions.getTargetRenderView();
        if (targetRenderView == null) {
            mView.mTargetRenderView = mView.mPromptOptions.getTargetView();
        } else {
            mView.mTargetRenderView = targetRenderView;
        }
        updateClipBounds();
        final Component targetView = mView.mPromptOptions.getTargetView();
        if (targetView != null) {
            mView.mPromptOptions.getPromptFocal().prepare(
                    mView.mPromptOptions, mView.getLocationOnScreen());
        } else {
            final Point targetPosition = mView.mPromptOptions.getTargetPosition();
            if (targetPosition!=null){

                mView.mPromptOptions.getPromptFocal().prepare(
                        mView.mPromptOptions, targetPosition.getPointX(), targetPosition.getPointY());
            }
        }
        mView.mPromptOptions.getPromptText().prepare(mView.mPromptOptions, mView.isClipToBounds, mView.mClipBounds);
        mView.mPromptOptions.getPromptBackground().prepare(
                mView.mPromptOptions, mView.isClipToBounds, mView.mClipBounds);
        updateIconPosition();
    }

    /**
     * Update the icon drawable position or target render view position.
     */
    void updateIconPosition() {
        Drawable iconDrawable = mView.mPromptOptions.getIconDrawable();
        if (iconDrawable != null) {
            mView.mIconDrawable = iconDrawable;
            final RectFloat focalBounds = mView.mPromptOptions.getPromptFocal().getBounds();
            Point center = focalBounds.getCenter();
            mView.mIconDrawableLeft = center.getPointX()
                    - (iconDrawable.getWidth() / Constants.NUM_2F);
            mView.mIconDrawableTop = center.getPointY()
                    - (iconDrawable.getHeight() / Constants.NUM_2F);
            ComponentContainer.LayoutConfig layoutConfig = iconDrawable.getLayoutConfig();
            layoutConfig.setMarginLeft((int) mView.mIconDrawableLeft);
            layoutConfig.setMarginTop((int) mView.mIconDrawableTop);
            mView.mIconDrawable.setLayoutConfig(layoutConfig);
        } else if (mView.mTargetRenderView != null) {
            final int[] viewPosition = mView.getLocationOnScreen();
            final int[] targetPosition = mView.mTargetRenderView.getLocationOnScreen();
            mView.mIconDrawableLeft = targetPosition[0]
                    - viewPosition[0] - mView.mTargetRenderView.getScrollValue(Component.AXIS_X);
            mView.mIconDrawableTop = targetPosition[1]
                    - viewPosition[1] - mView.mTargetRenderView.getScrollValue(Component.AXIS_Y);
        }
    }

    /**
     * Update the bounds that the prompt is clip to.
     */
    void updateClipBounds() {
        final Component clipToView = mView.mPromptOptions.getClipToView();
        if (clipToView != null) {
            mView.isClipToBounds = true;

            // Reset the top to 0
            mView.mClipBounds.set(0, 0, 0, 0);

            // Find the location of the clip view on the screen
            clipToView.getSelfVisibleRect(mView.mClipBounds);
        } else {
            mView.mPromptOptions.getResourceFinder().getPromptParentView().getWindowVisibleRect(mView.mClipBounds);
            mView.isClipToBounds = false;
            mView.mClipBounds.top = 0;
        }
    }

    /**
     * Handles emitting the prompt state changed events.
     *
     * @param state The state that the prompt is now in.
     */
    protected void onPromptStateChanged(final int state) {
        mState = state;
        mView.mPromptOptions.onPromptStateChanged(this, state);
        mView.mPromptOptions.onExtraPromptStateChanged(this, state);
    }

    /**
     * Creates a prompt with the supplied options.
     *
     * @param promptOptions The options to use to create the prompt.
     * @return The created prompt.
     */
    public static MaterialTapTargetPrompt createDefault(final PromptOptions promptOptions) {
        return new MaterialTapTargetPrompt(promptOptions);
    }

    /**
     * View used to render the tap target.
     *
     * @since 2021-06-01
     */
    static class PromptView extends Component implements Component.DrawTask,
            Component.TouchEventListener, Component.KeyEventListener, Component.BindStateChangedListener {
        Drawable mIconDrawable;
        float mIconDrawableLeft;
        float mIconDrawableTop;
        PromptTouchedListener mPromptTouchedListener;
        Rect mClipBounds = new Rect();
        Component mTargetRenderView;
        MaterialTapTargetPrompt mPrompt;
        PromptOptions mPromptOptions;
        boolean isClipToBounds;
        PromptViewHandler mPromptViewHandler;

        /**
         * Create a new prompt view.
         *
         * @param context The context that the view is created in.
         */
        public PromptView(final Context context) {
            super(context);
            setTouchFocusable(true);
            requestFocus();
            EventRunner eventRunner = EventRunner.getMainEventRunner();
            mPromptViewHandler = new PromptViewHandler(eventRunner);
            addDrawTask(this);
            setTouchEventListener(this);
            setBindStateChangedListener(this);
            setKeyEventListener(this);
        }

        /**
         * OnCompleteHandler
         *
         * @since 2021-04-13
         */
        static class PromptViewHandler extends EventHandler {
            PromptViewHandler(EventRunner runner) throws IllegalArgumentException {
                super(runner);
            }

            @Override
            protected void processEvent(InnerEvent event) {
                super.processEvent(event);
            }
        }

        /**
         * postDelayed
         *
         * @param runnable runnable
         * @param millis   millis
         */
        public void postDelayed(Runnable runnable, long millis) {
            mPromptViewHandler.postTask(runnable, millis);
        }

        /**
         * removeCallbacks
         *
         * @param runnable runnable
         */
        public void removeCallbacks(Runnable runnable) {
            mPromptViewHandler.removeTask(runnable);
        }

        @Override
        public void onDraw(Component component, Canvas canvas) {
            if (isClipToBounds) {
                canvas.clipRect(mClipBounds);
            }

            // Draw the backgrounds, clipping the focal path so we don't draw over it.
            final Path focalPath = mPromptOptions.getPromptFocal().getPath();
            if (focalPath != null) {
                canvas.save();
                canvas.clipPath(focalPath, Canvas.ClipOp.DIFFERENCE);
            }

            mPromptOptions.getPromptBackground().draw(canvas);

            if (focalPath != null) {
                canvas.restore();
            }

            // Draw the focal
            mPromptOptions.getPromptFocal().draw(canvas);

            // Draw the text
            mPromptOptions.getPromptText().draw(canvas);
        }

        @Override
        public boolean onTouchEvent(Component component, TouchEvent event) {
            final float xx = event.getPointerPosition(event.getIndex()).getX();
            final float yy = event.getPointerPosition(event.getIndex()).getY();

            // If the touch point is within the prompt background stop the event from passing through it
            boolean captureEvent = (!isClipToBounds || mClipBounds.isInclude((int) xx, (int) yy))
                    && mPromptOptions.getPromptBackground().contains(xx, yy);
            setClickedListener(null);

            // If the touch event was at least in the background and in the focal
            if (captureEvent && mPromptOptions.getPromptFocal().contains(xx, yy)) {
                // Override allowing the touch event to pass through the view with the user defined value
                captureEvent = mPromptOptions.getCaptureTouchEventOnFocal();
                if (mPromptTouchedListener != null) {
                    mPromptTouchedListener.onFocalPressed();
                }
            } else if (captureEvent && mPromptOptions.getCaptureTouchEventOnBackgroundPrompt()) {
                // If the prompt background was not touched
                setClickedListener(component1 -> {
                });
                if (mPromptTouchedListener != null) {
                    mPromptTouchedListener.onFocalBackgroundPressed();
                }
            } else {
                if (!captureEvent) {
                    captureEvent = mPromptOptions.getCaptureTouchEventOutsidePrompt();
                }
                if (mPromptTouchedListener != null) {
                    mPromptTouchedListener.onNonFocalPressed();
                }
            }
            return captureEvent;
        }

        @Override
        public boolean onKeyEvent(Component component, KeyEvent keyEvent) {
            if (mPromptOptions.getBackButtonDismissEnabled()
                    && keyEvent.getKeyCode() == KeyEvent.KEY_BACK) {
                if (keyEvent.isKeyDown()) {
                    return true;
                } else if (!keyEvent.isKeyDown()) {
                    if (mPromptTouchedListener != null) {
                        mPromptTouchedListener.onBackButtonPressed();
                    }
                    return mPromptOptions.getAutoDismiss()
                            || super.getKeyEventListener().onKeyEvent(component, keyEvent);
                }
            }
            return super.getKeyEventListener().onKeyEvent(component, keyEvent);
        }

        @Override
        public void onComponentBoundToWindow(Component component) {
        }

        @Override
        public void onComponentUnboundFromWindow(Component component) {
            mPrompt.cleanUpAnimation();
        }

        @TestOnly
        public PromptOptions getPromptOptions() {
            return mPromptOptions;
        }

        /**
         * Interface definition for a callback to be invoked when a {@link PromptView} is touched.
         *
         * @since 2021-06-01
         */
        public interface PromptTouchedListener {
            /**
             * Called when the focal is pressed.
             */
            void onFocalPressed();

            /**
             * Called when the FocalBackground is pressed.
             */
            void onFocalBackgroundPressed();

            /**
             * Called when anywhere outside the focal is pressed or the system back button is
             * pressed.
             */
            void onNonFocalPressed();

            /**
             * Called when the system back button is pressed.
             */
            void onBackButtonPressed();
        }
    }

    /**
     * A builder to create a {@link MaterialTapTargetPrompt} instance.
     *
     * @since 2021-06-01
     */
    public static class Builder extends PromptOptions<Builder> {
        /**
         * Creates a builder for a tap target prompt that uses the default tap target prompt theme.
         *
         * @param fraction           the Fraction to show the prompt within.
         * @param componentContainer componentContainer
         * @see #Builder(Fraction, ComponentContainer, StyleBean)
         */
        public Builder(final Fraction fraction, ComponentContainer componentContainer) {
            this(fraction, componentContainer, null);
        }

        /**
         * Creates a builder for a material tap target prompt that uses an explicit theme resource.
         * <p>
         * The {@code themeResId} may be specified as {@code 0} to use the parent {@code context}'s
         * resolved value for {@link uk.co.samuelwall.materialtaptargetprompt}.
         *
         * @param fraction           the Fraction to show the prompt within.
         * @param styleBean          the resource ID of the theme against which to inflate this dialog, or
         * @param componentContainer componentContainer
         *                           {@code 0} to use the parent {@code context}'s default material tap
         *                           target prompt theme
         */
        public Builder(final Fraction fraction, ComponentContainer componentContainer, StyleBean styleBean) {
            this(new SupportFractionResourceFinder(fraction, componentContainer), styleBean);
        }

        /**
         * Creates a builder for a tap target prompt that uses the default tap target prompt theme.
         *
         * @param component the dialog to show the prompt within.
         * @param ability   ability.
         */
        public Builder(final ComponentContainer component, final Ability ability) {
            this(component, ability, null);
        }

        /**
         * Creates a builder for a material tap target prompt that uses an explicit theme resource.
         * <p>
         * The {@code themeResId} may be specified as {@code 0} to use the parent {@code context}'s
         * resolved value for {@link uk.co.samuelwall.materialtaptargetprompt}.
         *
         * @param component the dialog to show the prompt within.
         * @param ability   ability
         * @param styleBean the resource ID of the theme against which to inflate this dialog, or
         *                  {@code 0} to use the parent {@code context}'s default material tap
         *                  target prompt theme
         */
        public Builder(ComponentContainer component, Ability ability, StyleBean styleBean) {
            this(new DialogResourceFinder(ability, component), styleBean);
        }

        /**
         * Creates a builder for a tap target prompt that uses the default tap target prompt theme.
         *
         * @param ability            the ability to show the prompt within.
         * @param componentContainer componentContainer
         */
        public Builder(final Ability ability, ComponentContainer componentContainer) {
            this(ability, componentContainer, null);
        }

        /**
         * Creates a builder for a material tap target prompt that uses an explicit theme resource.
         * <p>
         * The {@code themeResId} may be specified as {@code 0} to use the parent {@code context}'s
         * resolved value for {@link uk.co.samuelwall.materialtaptargetprompt}.
         *
         * @param ability            the ability to show the prompt within.
         * @param styleBean          the resource ID of the theme against which to inflate this dialog, or
         * @param componentContainer componentContainer
         *                           {@code 0} to use the parent {@code context}'s default material tap
         *                           target prompt theme
         */
        public Builder(final Ability ability, ComponentContainer componentContainer, StyleBean styleBean) {
            this(new AbilityResourceFinder(ability, componentContainer), styleBean);
        }

        /**
         * Creates a builder for a material tap target prompt that uses an explicit theme resource.
         * <p>
         * The {@code themeResId} may be specified as {@code 0} to use the parent {@code context}'s
         * resolved value for {@link uk.co.samuelwall.materialtaptargetprompt}.
         *
         * @param resourceFinder The {@link ResourceFinder} used to find views and resources.
         * @param styleBean      the resource ID of the theme against which to inflate this dialog,
         *                       or {@code 0} to use the parent {@code context}'s default material
         *                       tap target prompt theme
         */
        public Builder(final ResourceFinder resourceFinder, StyleBean styleBean) {
            super(resourceFinder);
            if (styleBean != null) {
                load(styleBean);
            }
        }
    }

    /**
     * Interface definition for a callback to be invoked when a prompts state changes.
     *
     * @since 2021-06-01
     */
    public interface PromptStateChangeListener {
        /**
         * Called when the prompts state changes.
         *
         * @param prompt The prompt which state has changed.
         * @param state  can be either {@link #STATE_REVEALING}, {@link #STATE_REVEALED}, {@link
         *               #STATE_FOCAL_PRESSED}, {@link #STATE_FINISHED}, {@link #STATE_DISMISSING},
         *               {@link #STATE_DISMISSED}
         */
        void onPromptStateChanged(MaterialTapTargetPrompt prompt, int state);
    }
}
